// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
/**
 * @class metrics::CounterMetric
 * @ingroup metrics
 *
 * @brief Counts a value that only moves upwards.
 *
 * NB! If you have a MetricSet subclass you want to create a sum for, use
 * MetricSet itself as the template argument. Otherwise you'll need to override
 * clone(...) in order to make it return the correct type for your
 * implementation.
 */

package com.yahoo.metrics;

import com.yahoo.text.XMLWriter;

import java.util.ArrayList;
import java.util.List;

public class SumMetric extends Metric
{
    ArrayList<Metric> metricsToSum = new ArrayList<Metric>();

    public SumMetric(String name, String tags, String description, MetricSet owner) {
        super(name, tags, description, owner);
        metricsToSum = new ArrayList<Metric>();
    }

    public SumMetric(SumMetric other, MetricSet owner) {
        super(other, owner);


        if (other.owner == null) {
            throw new IllegalStateException(
                    "Cannot copy a sum metric not registered in a metric set, as " +
                            "we need to use parent to detect new metrics to point to.");
        }
        if (owner == null) {
            throw new IllegalStateException(
                    "Cannot copy a sum metric directly. One needs to at least " +
                            "include metric set above it in order to include metrics " +
                            "summed.");
        }

        metricsToSum.ensureCapacity(other.metricsToSum.size());
        List<String> parentPath = other.owner.getPathVector();

        for (Metric metric : other.metricsToSum) {
            List<String> addendPath = metric.getPathVector();
            MetricSet newAddendParent = owner;

            for (int i = parentPath.size(); i < addendPath.size() - 1; ++i) {
                String path = addendPath.get(i);
                Metric child = newAddendParent.getMetric(path);
                if (child == null) {
                    throw new IllegalStateException(
                            "Metric " + path + " in metric set "
                                    + newAddendParent.getPath() + " was expected to " +
                                    "exist. This sounds like a bug.");
                }

                newAddendParent = (MetricSet)child;
            }

            String path = addendPath.get(addendPath.size() - 1);
            Metric child = newAddendParent.getMetric(path);
            if (child == null) {
                throw new IllegalStateException(
                        "Metric " + path + " in metric set "
                                + newAddendParent.getPath() + " was expected to " +
                                "exist. This sounds like a bug.");
            }

            metricsToSum.add(child);
        }
    }

    @Override
    public boolean visit(MetricVisitor visitor, boolean tagAsAutoGenerated) {
        if (metricsToSum.isEmpty()) return true;

        Metric sum = generateSum();

        if (sum == null) {
            return true;
        }

        if (sum instanceof MetricSet) {
            sum.visit(visitor, true);
            return true;
        } else {
            return visitor.visitPrimitiveMetric(sum, true);
        }
    }

    @Override
    public Metric clone(CopyType copyType, MetricSet owner, boolean includeUnused) {
        if (copyType == CopyType.CLONE) {
            return new SumMetric(this, owner);
        }

        Metric sum = null;
        for (Metric metric : metricsToSum) {
            if (sum == null) {
                sum = metric.clone(CopyType.INACTIVE, null, includeUnused);
                sum.setName(getName());
                sum.setDescription(getDescription());
                sum.setTags(getTags());

                if (owner != null) {
                    owner.registerMetric(sum);
                }
            } else {
                metric.addToPart(sum);
            }
        }

        return sum;
    }

    @Override
    public void addToPart(Metric partMetric) {
        Metric m = generateSum();
        if (m != null) {
            m.addToPart(partMetric);
        }
    }

    @Override
    public void addToSnapshot(Metric snapshotMetric) {
        Metric m = generateSum();
        if (m != null) {
            m.addToSnapshot(snapshotMetric);
        }
    }

    public void addMetricToSum(Metric metric) {
        if (owner == null) {
            throw new IllegalStateException(
                    "Sum metric needs to be registered in a parent metric set " +
                            "prior to adding metrics to sum.");
        }
        if (!metricsToSum.isEmpty() && !(metric.getClass().equals(metricsToSum.get(0).getClass()))) {
            throw new IllegalStateException(
                    "All metrics in a metric set must be of the same type.");
        }

        List<String> sumParentPath = owner.getPathVector();
        List<String> addedPath = metric.getPathVector();

        boolean error = false;
        if (addedPath.size() <= sumParentPath.size()) {
            error = true;
        } else for (int i=0; i<sumParentPath.size(); ++i) {
            if (!sumParentPath.get(i).equals(addedPath.get(i))) {
                error = true;
                break;
            }
        }
        if (error) {
            throw new IllegalStateException(
                        "Metric added to sum is required to be a child of the sum's " +
                        "direct parent metric set. (Need not be a direct child) " +
                        "Metric set " + metric.getPath() + " is not a child of " +
                        owner.getPath());
        }

        ArrayList<Metric> metrics = new ArrayList<Metric>(metricsToSum.size() + 1);
        for (int i = 0; i < metricsToSum.size(); ++i) {
            metrics.add(metricsToSum.get(i));
        }
        metrics.add(metric);
        metricsToSum = metrics;
    }

    public void removeMetricFromSum(Metric metric) {
        metricsToSum.remove(metric);
    }

    public Metric generateSum() {
        Metric m = clone(CopyType.INACTIVE, null, true);

        if (m != null) {
            m.owner = owner;
        }
        return m;
    }

    @Override
    public long getLongValue(String id) {
        Metric s = generateSum();
        if (s == null) {
            return 0;
        }
        return s.getLongValue(id);
    }

    @Override
    public double getDoubleValue(String id) {
        Metric s = generateSum();
        if (s == null) {
            return 0.0;
        }
        return generateSum().getDoubleValue(id);
    }

    @Override
    public void logEvent(EventLogger logger, String fullName) {
        Metric s = generateSum();
        if (s != null) {
            s.logEvent(logger, fullName);
        }
    }

    @Override
    public void printXml(XMLWriter writer, int secondsPassed, int verbosity) {
        Metric s = generateSum();
        if (s == null) {
            openXMLTag(writer, verbosity);
            writer.closeTag();
            return;
        }

        generateSum().printXml(writer, secondsPassed, verbosity);
    }

    @Override
    public boolean used() {
        for (Metric m : metricsToSum) {
            if (m.used()) return true;
        }
        return false;
    }

    @Override
    public void reset() {}
}
